package com.liuhuxiang.service;

import com.alibaba.fastjson.JSON;
import com.liuhuxiang.common.RedisKeyPrefixConst;
import com.liuhuxiang.common.RedisUtil;
import com.liuhuxiang.dao.ProductDao;
import com.liuhuxiang.model.Product;
import jodd.util.StringUtil;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Service
public class ProductService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private Redisson redisson;

    /***
     * 缓存失效时间 24H
     */
    public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;

    /***
     * 空缓存
     */
    public static final String               EMPTY_CACHE                   = "{}";

    /***
     * 热点商品缓存前缀
     */
    public static final String               LOCK_PRODUCT_HOT_CACHE_PREFIX = "lock:product:hot_cache:";

    /***
     * 数据库缓存双写不一致的key
     */
    public static final String               LOCK_PRODUCT_UPDATE_PREFIX    = "lock:product:update:";

    /***
     * JVM内存级别缓存（简单的使用map来演示，实际使用的时候，不能用map，因为map有大小限制，这里只做演示）
     * JVM进程级别的缓存，可以使用Ehcache
     */
    public static Map<String, Product> productMap = new ConcurrentHashMap<>();

    @Transactional
    public Product create(Product product) {
        Product productResult = null;
        //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        //updateProductLock.lock();
        /******优化分布式锁，用读写锁---写的场景******/
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RLock wlocak = readWriteLock.writeLock();
        wlocak.lock();
        try {
            // 新增产品
            productResult = productDao.create(product);
            // 产品新增完成后加入缓存
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);

            //多级缓存架构：放入JVM进程级别缓存（用EhCache，不能用map），这里map只做演示
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(),product);
        } finally {
            wlocak.unlock();
        }
        return productResult;
    }

    @Transactional
    public Product update(Product product) {
        Product productResult = null;
        //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        //updateProductLock.lock();
        /******优化分布式锁，用读写锁---写的场景******/
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RLock wlocak = readWriteLock.writeLock();
        wlocak.lock();
        try {
            // 修改产品
            productResult = productDao.update(product);

            // 产品修改完成后，覆盖缓存
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);


            //多级缓存架构：放入JVM进程级别缓存（用EhCache，不能用map），这里map只做演示
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(),product);
        } finally {
            wlocak.unlock();
        }
        return productResult;
    }

    /***
     * 查询商品逻辑
     *
     * @param productId
     * @return
     * @throws InterruptedException
     */
    public Product get(Long productId) throws InterruptedException {
        Product product = null;

        //缓存的key
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        // 先从缓存中查询
        product = getProductFromCache(productCacheKey);
        // 缓存不为空
        if (product != null) {

            //表明当前产品不是冷门商品，延期锁的过期时间
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            return product;
        }
        /**************下面的代码，是解决小概率的并发问题，90%的场景，只用走上面的逻辑即可，效率能够保证*********/

        //双重校验，改用分布式锁
        /***这把锁用来解决突发热点缓存重建***/
        RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
        hotCacheLock.lock();

        try {
            //DCL模式，第二次获取：从缓存中获取数据
            product = getProductFromCache(productStr);
            if(product!=null){
                return product;
            }

            /***这把锁用来解决DB 缓存 双写不一致问题***/
            //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            //updateProductLock.lock();
            /******优化分布式锁，用读写锁---读的场景******/
            RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            RLock rLock = readWriteLock.readLock();
            rLock.lock();
            try {
                //在从DB中查询
                product = productDao.get(productId);
                if (product != null) {
                    //从DB中查到了，就放到缓存中，并且设置过期时间为24H
                    redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECONDS);

                    //多级缓存架构：放入JVM进程级别缓存（用EhCache，不能用map），这里map只做演示
                    productMap.put(productCacheKey,product);
                } else {
                    // DB中不存在，就放到空缓存中，防止黑客恶意的请求不存在的商品，导致缓存穿透的情况
                    // 也要设置延期时间，这种延期时间就设置短一点即可
                    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                }
            } finally {
                /***这把锁用来解决DB 缓存 双写不一致问题***/
                //updateProductLock.unlock();
                rLock.unlock();
            }

        } finally {
            hotCacheLock.unlock();
        }

        return product;
    }

    /****
     * 根据key从缓存中获取值
     *
     * @param productCacheKey
     * @return
     */
    private Product getProductFromCache(String productCacheKey) {
        Product product = null;

        /*****多级缓存架构，查询的时候，先从内存中查询******/
        if (productMap.get(productCacheKey) != null) {
            return product;
        }

        // 二级缓存：从redis缓存中查询
        String productStr  = redisUtil.get(productCacheKey);
        // 缓存不为空
        if (productStr != null) {
            //如果查询到的是空缓存
            if (StringUtil.equals(EMPTY_CACHE, productStr)) {
                //即便空缓存，查到也要延期，防止黑客一致请求，不延期下次还要走DB
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                //return null; 要与前端约定好 空对象和null分别提示用户什么信息
                return new Product();
            }
            //表明当前产品不是冷门商品，延期锁的过期时间
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
            return product;
        }
        return product;
    }
    /***
     * 空缓存的过期时间设置
     * @return
     */
    private Integer genEmptyCacheTimeout() {
        return 60 + new Random().nextInt(30);
    }

    /****
     * 在原先24小时的基础之上，随机的加上5小时内的时间
     * @return
     */
    private Integer genProductCacheTimeout() {
        // PRODUCT_CACHE_TIMEOUT=24小时  + 5小时内的任意时间
        return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
    }


}